package agents.resource;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import agents.Agent;
import agents.Ant;
import agents.exceptions.ActionFailedException;
import agents.resource.exceptions.BookingFailedException;
import agents.resource.exceptions.CollisionException;
import domain.Car;
import domain.Clock;
import domain.Segment;
import domain.TimeInterval;
import domain.TrafficInfrastructure;

public abstract class ResourceAgent implements Agent {
	
	private static final int SEND_ANT_INTERVAL = 7;
	private long timeToSendNextAnt;
	private static final int CROSSROAD_DISTANCE_TIME_TO_EXPIRE = SEND_ANT_INTERVAL + 3; //vervaltijd van informatie over wegennet 
	private static final int UPDATE_OLD_INFORMATION = 4; // tijd wanneer ant mag doorgelaten worden (als hij informatie van UPDATE_OLD_INFORMATION oud vernieuwt)
	private static final float PENALTY_BIRD_FLIGHT = 1.5f;
	private static final float PENALTY_TRAFFIC = 1.8f;
	private static final long TIME_TO_EXPIRATION = 2; // booking time to expiration 
	
	protected ResourceAgent(){
		timeToSendNextAnt = Clock.getInstance().getCurrentTime(); //onmiddelijk feasibility ant zenden
	}
	
	public void think(){
		antsWithNewInformation.clear();
		removeOldTrafficDistances();
	}
	
	private List<Booking> bookings = new ArrayList<Booking>();
	
	protected List<Booking> getAllBookings() {
		long currentTime = Clock.getInstance().getCurrentTime();
		List<Booking> allBookings = new ArrayList<Booking>(bookings);
		for(Booking b : allBookings) {
			//verwijderen wanneer booking vervallen is, gepasseerd is of auto heeft exitbooking
			if(b.getExpirationTime() <= currentTime || b.getTimeInterval().getEndTime() < currentTime || hasExitBooking(b.getCar())) {
				bookings.remove(b);
			}
		}
		return new ArrayList<Booking>(bookings);
	}
	
	/**
	 * Check of de opgegeven auto zijn exit uit de resource heeft geboekt
	 * @param car
	 * @return
	 */
	protected abstract boolean hasExitBooking(Car car);
	
	protected List<Booking> getAllBookingsNotFrom(Car car) {
		List<Booking> bookings = getAllBookings();
		for(Booking b : getAllBookings()) {
			if(b.getCar() == car) bookings.remove(b);
		}
		return bookings;
	}
	
	protected void addBooking(Booking b) {
		bookings.add(b);
	}
	protected void addBooking(int i, Booking b){
		bookings.add(i,b);
	}
	protected void removeBooking(Booking b) {
		bookings.remove(b);
	}
	
	
	//checkt ofdat al geboekt is, zoneen legt vast voor gegeven agent
	//agent al een andere booking heeft verwijderen
	public void book(TimeInterval interval, Car car) throws BookingFailedException {
		if(getFreeIntervals(interval.getStartTime(), car).get(0).getStartTime() != interval.getStartTime()) {
			throw new BookingFailedException("Starttijd is onmogelijk");
		}
		if(!getPossibleExitInterval(interval.getStartTime(), car).contains(interval.getEndTime())){
			throw new BookingFailedException("Endtijd is onmogelijk");
		}
		
		removeCarBooking(car);
		
		//booking ertussen voegen
		for(int i = 0; i< bookings.size(); i++){
			if(bookings.get(i).getTimeInterval().getStartTime() >interval.getStartTime()){
				addBooking(i,new Booking(interval,car));
				return;
			}
		}
		//booking wordt op het einde toegevoegd, booking komt als laatste
		addBooking(new Booking(interval, car));
		
	}


	private void removeCarBooking(Car car) {
		removeBooking(getBookingFromCar(car));
	}
	
	protected Booking getBookingFromCar(Car car) {
		for(Booking book : getAllBookings()){
			if(book.getCar().equals(car)){
				return book;
			}
		}
		return null;
	}
	
	//beweegt naar opgegeven uitgangsresource
	public abstract void moveTo(Car car, ResourceAgent neighbour) throws ActionFailedException; 
	
	
	//geeft vrije intervallen terug om op trafficInfrastructure te kunnen
	public abstract List<TimeInterval> getFreeIntervals(long startTime, Car car);
	
	/**	Tijd vanaf wanneer auto op de resource weg kan en tot wanneer deze het laast op deze resource mag zijn
	 * Mag enkel opgeroepen worden met auto's die zich (nog) niet op de resource bevinden
	 * @param enterTime
	 * @param car
	 * @return
	 */
	public abstract TimeInterval getPossibleExitInterval(long enterTime, Car car);
	
	//stelt een booking van de resource voor een bepaald tijdsinterval voor
	protected class Booking {
		//heeft timeinterval, taskagent
		protected Booking(TimeInterval time, Car car){
			this.timeInterval = time;
			this.car = car;
			this.expirationTime = Clock.getInstance().getCurrentTime() + TIME_TO_EXPIRATION;
		}
		
		private TimeInterval timeInterval;
		private Car car;
		
		private long expirationTime;
		
		protected long getExpirationTime() {
			return expirationTime;
		}
		
		protected TimeInterval getTimeInterval(){
			return timeInterval;
		}
		
		protected void setTimeInterval(TimeInterval newTimeInterval) {
			timeInterval = newTimeInterval;
		}
		
		protected Car getCar(){
			return car;
		}
		
		@Override
		public String toString() {
			return "by " + car.toString() + " at " + timeInterval.toString();
		}
	}
	
	//tijdsinterval dat auto op resource deze kan/mag verlaten
	public abstract TimeInterval getDesiredExitTime(Car car);

	
	/* *** resources komend en uitgaand van deze resource *** */
	public abstract List<ResourceAgent> getIngoingResources();
	public abstract List<ResourceAgent> getOutgoingResources();
	abstract void addOutgoingResource(ResourceAgent out);
	abstract void addIngoingResource(ResourceAgent in);
	abstract void removeOutgoingResource(ResourceAgent out);
	abstract void removeIngoingResource(ResourceAgent in);
	
	//verwijdert de resource van de omgeving; verbreekt verbindingen met de incoming/outgoing buren
	//enkel voor gebruik door environment
	public void removeResource() {
		if (hasCars()) throw new IllegalStateException("cars are still on resource while trying to remove it"); 
		
		for(ResourceAgent out : getOutgoingResources()) {
			out.removeIngoingResource(this);
			removeOutgoingResource(out);
		}
		for(ResourceAgent in : getIngoingResources()) {
			in.removeOutgoingResource(this);
			removeIngoingResource(in);
		}
		
		isRemoved = true;
	}
	
	public boolean isRemoved() {
		return isRemoved;
	}
	
	private boolean isRemoved = false;
	
	public boolean hasCars() {
		for(Segment s : getTrafficInfrastructure().getSegments()) {
			if(s.getCar() != null) return true;
		}
		return false;
	}
	
	/**
	 * Geeft terug ofdat gegeven auto zich op de resource bevindt.
	 * @param car
	 * @return
	 */
	public boolean hasCar(Car car) {
		for(Segment s : getTrafficInfrastructure().getSegments()) {
			if(s.getCar() == car) return true;
		}
		return false;
	}
	
	//zet een auto op het begin van de resource
	public abstract void addCar(Car car) throws CollisionException;
	
	public boolean hasSegment(Segment seg) {
		return getTrafficInfrastructure().hasSegment(seg);
	}

	public abstract TrafficInfrastructure getTrafficInfrastructure();
	
	@Override
	public String toString() {
		return getTrafficInfrastructure().getId();
	}
	
	/*************************
	 * Feasibility Ants Info *
	 *************************/
	
	private List<ResourceDistance> trafficInfrastructureDistances = new ArrayList<ResourceDistance>();
	
	private class ResourceDistance {
		private TrafficInfrastructure infrastruc;
		private float distance;
		private long creationTime;
		private long timeLastAntSent;
		private final ArrayList<TrafficInfrastructure> outgoingResources;
		
		public ResourceDistance(TrafficInfrastructure cr, float distance, Collection<TrafficInfrastructure> outs){
			this.infrastruc = cr;
			this.distance = distance;
			this.creationTime = Clock.getInstance().getCurrentTime();
			updateTimeLastAntSent();
			this.outgoingResources = new ArrayList<TrafficInfrastructure>(outs);
		}
		
		public TrafficInfrastructure getTrafficInfrastructure(){
			return infrastruc;
		}
		public float getDistance(){
			return distance;
		}
		public long getCreationTime(){
			return creationTime;
		}
		private void resetCreationTime(){
			creationTime = Clock.getInstance().getCurrentTime();
		}
		
		public void updateTimeLastAntSent() {
			timeLastAntSent = Clock.getInstance().getCurrentTime();
		}
		
		//tijd laatste feasibility ant met informatie verzonden
		public long getTimeLastAntSent() {
			return timeLastAntSent;
		}
		
		@Override
		public String toString() {
			return infrastruc.toString() + " at dist " + distance + " and created " + creationTime;
		}

		public ArrayList<TrafficInfrastructure> getOutgoingNeighbours() {
			return new ArrayList<TrafficInfrastructure>(outgoingResources);
		}
	}

	public List<ResourceDistance> getTrafficInfrastructureDistances(){
		return trafficInfrastructureDistances;
	}
	
	private ResourceDistance findResourceDistance(TrafficInfrastructure infr, float dist){
		for(ResourceDistance rDis : trafficInfrastructureDistances){
			if(rDis.getTrafficInfrastructure() == infr && rDis.getDistance() == dist)
				return rDis;
		}
		return null;
	}
	
	//haalt info uit ant
	//oude info => executeert ant
	//nieuwe info => laat ant door
	// enkel de antsToSend worden doorgelaten, de andere worden getermineerd
	public void notifyFeasibilityAntArrival(FeasibilityAnt arrivedAnt) {
		List<TrafficInfrastructure> resourcesToCheck = arrivedAnt.getPassedTrafficInfrastructure();
		for(TrafficInfrastructure infr : resourcesToCheck){
			ResourceDistance resDis = findResourceDistance(infr,arrivedAnt.getDistanceTo(infr));
			// heeft een update van oude informatie
			if(resDis != null){
				// moet na een tijd terug ant doorlaten, anders zal volgende crossroad na een tijd niets meer weten
				if(resDis.getTimeLastAntSent() + UPDATE_OLD_INFORMATION <= Clock.getInstance().getCurrentTime()){
					antsWithNewInformation.add(arrivedAnt);
					resDis.updateTimeLastAntSent();
					timeToSendNextAnt = Clock.getInstance().getCurrentTime()+ SEND_ANT_INTERVAL;
				}
				resDis.resetCreationTime();
			}
			// heeft nieuwe informatie, altijd doorlaten
			else{
				trafficInfrastructureDistances.add(new ResourceDistance(infr,arrivedAnt.getDistanceTo(infr),arrivedAnt.getOutgoingResourcesOf(infr)));
				antsWithNewInformation.add(arrivedAnt);
				timeToSendNextAnt = Clock.getInstance().getCurrentTime() + SEND_ANT_INTERVAL;
			}
		}	

		/*System.out.println(this.toString());
		System.out.println("   " + trafficInfrastructureDistances.toString());*/
	}

	//	schatting van de tijd dat het duurt om destination te bereiken vanaf het einde van deze resource
	//berekent ahv informatie van feasibility agents + evt schatting ahv coordinaten(vogelvlucht)	
	//schatting van de tijd dat het duurt om destination te bereiken
	//berekent ahv informatie van feasibility agents + evt schatting ahv coordinaten(vogelvlucht)	
	public long getEstimatedTimeTo(Segment destination) {
		
		for(ResourceDistance dis : getShortestTrafficInfrastructureDistances()){
			if(dis.getTrafficInfrastructure().hasSegment(destination)) {
				//	als segment in dit traffic structure zit, gewoon afstand teruggeven
				return (long) (PENALTY_TRAFFIC * dis.getDistance());
			}
		}
		
		// segment is niet gekend, infrastructure het dichtste ertegen zoeken en tijd in vogelvlucht bij optellen
		return getBirdFlightTime(destination);
	}

	//beste schatting tot destinatie waarbij een afstand in vogelvlucht is in meegerekend
	//is -1 wanneer destinatie onbereikbaar blijkt
	private long getBirdFlightTime(Segment destination) {
		Collection<ResourceDistance> resDistances = getDistOfOuterTrafficInfrastructure();
		
		float minBirdFlight = Float.POSITIVE_INFINITY;
		long timeToDestination = -1;
		
		for(ResourceDistance resDis : resDistances) {
			Segment lastSegment = resDis.getTrafficInfrastructure().getSegments().get(resDis.getTrafficInfrastructure().getSegments().size()-1);
			float vogelvlucht = (float) Math.pow((Math.pow((Math.abs(destination.getX() - lastSegment.getX())),2) + Math.pow((Math.abs(destination.getY() - lastSegment.getY())),2)),0.5f);
			if(vogelvlucht < minBirdFlight) {
				minBirdFlight = vogelvlucht;
				timeToDestination = (long) (PENALTY_TRAFFIC * (PENALTY_BIRD_FLIGHT * vogelvlucht + resDis.getDistance())); 
			}
		}

		return timeToDestination;
	}

	private Collection<ResourceDistance> getDistOfOuterTrafficInfrastructure() {
		Collection<ResourceDistance> resDistances= getShortestTrafficInfrastructureDistances();
		for(ResourceDistance resDist : getShortestTrafficInfrastructureDistances()) {
			if(hasAllNeighbours(resDist)) resDistances.remove(resDist);
		}
		return resDistances;
	}

	private boolean hasAllNeighbours(ResourceDistance resDist) {
		for(TrafficInfrastructure infr : resDist.getOutgoingNeighbours()) {
			if(!hasInfrastructureInTable(infr)) return false;
		}
		return true;
	}

	private boolean hasInfrastructureInTable(TrafficInfrastructure infr) {
		for(ResourceDistance resDis : getTrafficInfrastructureDistances()) {
			if(resDis.getTrafficInfrastructure() == infr) return true;
		}
		return false;
	}

	private Collection<ResourceDistance> getShortestTrafficInfrastructureDistances() {
		Collection<ResourceDistance> resDistances = getTrafficInfrastructureDistances();
		Map<TrafficInfrastructure, ResourceDistance> shortestDistances = new HashMap<TrafficInfrastructure, ResourceDistance>();
		for(ResourceDistance resDis : resDistances) {
			ResourceDistance shortest = shortestDistances.get(resDis.getTrafficInfrastructure());
			if(shortest == null || shortest.getDistance() > resDis.getDistance())
				shortestDistances.put(resDis.getTrafficInfrastructure(), resDis);
		}
		return shortestDistances.values();
	}

	private void removeOldTrafficDistances() {
		List<ResourceDistance> trafficdiscopy = new ArrayList<ResourceDistance>(trafficInfrastructureDistances);
		for(ResourceDistance rsdis : trafficdiscopy) {
			if(rsdis.getCreationTime() + CROSSROAD_DISTANCE_TIME_TO_EXPIRE < Clock.getInstance().getCurrentTime()) {
				trafficInfrastructureDistances.remove(rsdis);
			}
		}
	}
	
	//	maakt feasibility ants aan
	//	stuurt feasibility ant wanneer timeSinceLastAntSent == SEND_ANT_INTERVAL
	public List<Ant> getAnts() {
		List<Ant> result = new ArrayList<Ant>();
		if(timeToSendNextAnt <= Clock.getInstance().getCurrentTime()){
			timeToSendNextAnt = Clock.getInstance().getCurrentTime() + SEND_ANT_INTERVAL;
			result.add(new FeasibilityAnt(this));
			return result;
		}
		return result; 
	}

	private Set<FeasibilityAnt> antsWithNewInformation = new HashSet<FeasibilityAnt>();
	
	public boolean hasNoUsefulInformation(FeasibilityAnt ant){
		return !antsWithNewInformation.contains(ant);
	}

	/**
	 * Geeft tijdstip wanneer een auto van de weg kan, als er geen verkeer zou zijn
	 */
	public abstract long getNoTrafficExitTime(Car car);
	
	/**
	 * Geeft eerst mogelijke exit time van auto, rekening houdend met de geboekte exit times van de auto's voor hem
	 * @param car
	 * @return
	 */
	public abstract long getFirstExitTimeOf(Car car);
	
	//boeken van het het verlaten van resource waar auto zich op bevindt
	public abstract void bookExitResource(long exitTime, Car car) throws BookingFailedException;
	
	/**
	 * Geeft laatste tijd dat auto's die nu op resource zijn, weggaan
	 * Geeft huidige tijd - 1 als geen auto's op resource deze geboekt hebben
	 * @return
	 */
	protected abstract long getLastBookedExitTime();

	public boolean canReach(Segment from, Segment to) {
		int start = getTrafficInfrastructure().getSegments().indexOf(from);
		int end = getTrafficInfrastructure().getSegments().indexOf(to);
		if(start == -1 || end == -1) throw new IllegalArgumentException("ni op de road");
		return end >= start;
	}
	
}